/*
 * Copyright (c) 2007 Team and Concepts Limited.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Team and Concepts Limited nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY TEAM AND CONCEPTS LIMITED ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL TEAM AND CONCEPTS LIMITED BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Text;
using EditGrid.API.Client.Atom;
using System.IO;
using System.Net;
using System.Web;

namespace EditGrid.API.Client
{
    /// <summary>
    /// Spreadsheet public access right in combined form.
    /// </summary>
    public enum PublicAccessRight
    {
        Private,
        PublicReadOnly,
        PublicReadWrite,
        PublicReadProtectedWrite,
        ProtectedReadOnly,
        ProtectedReadWrite
    }

    /// <summary>
    /// Represents a spreadsheet resource on EditGrid.
    /// </summary>
    public class Spreadsheet : EditGridEntity
    {
        private EditGridService service;

        internal Spreadsheet(EditGridService service, AtomEntry spreadsheetEntry)
            : base(spreadsheetEntry)
        {
            this.service = service;
        }

        #region Properties
        /// <summary>
        /// Returns the name of the spreadsheet represented by this object.
        /// </summary>
        public string SpreadsheetName
        {
            get { return this.GetValue("spreadsheet-name"); }
        }

        public long SpreadsheetId
        {
            get { return long.Parse(this.GetValue("spreadsheet-id")); }
        }

        public long SpreadsheetRevisionId
        {
            get { return long.Parse(this.GetValue("spreadsheet-revision-id")); }
        }

        /// <summary>
        /// Returns the name of the organisation owning the spreadsheet represented by this object.
        /// </summary>
        public string OwnerOrganisation
        {
            get { return this.GetValue("owner.organisation"); }
        }

        /// <summary>
        /// Returns the name of the workspace owning the spreadsheet represented by this object.
        /// </summary>
        public string OwnerWorkspace
        {
            get { return this.GetValue("owner.workspace"); }
        }

        /// <summary>
        /// Returns the public readability of the spreadsheet represented by this object.
        /// </summary>
        /// <value>True if the spreadsheet represented by this object is public readable or
        /// protected read</value>
        public bool PublicReadable
        {
            get { return this.GetBooleanValue("public-access.readable"); }
        }

        /// <summary>
        /// Returns the public writablility of the spreadsheet represented by this object.
        /// </summary>
        /// <value>True if the spreadsheet represented by this object is public writable or
        /// protected write</value>
        public bool PublicWritable
        {
            get { return this.GetBooleanValue("public-access.writable"); }
        }

        /// <summary>
        /// Returns the protected readability of the spreadsheet represented by this object.
        /// </summary>
        /// <value>True if the spreadsheet represented by this object is readable by users
        /// with a valid password</value>
        public bool ProtectedRead
        {
            get { return this.GetBooleanValue("public-access.protected-read"); }
        }

        /// <summary>
        /// Returns the protected writability of the spreadsheet represented by this object.
        /// </summary>
        /// <value>True if the spreadsheet represented by this object is writable by users
        /// with a valid password</value>
        public bool ProtectedWrite
        {
            get { return this.GetBooleanValue("public-access.protected-write"); }
        }

        /// <summary>
        /// Returns the combined public access right of the spreadsheet represented by this object.
        /// </summary>
        public PublicAccessRight PublicAccessRight
        {
            get
            {
                if (this.ProtectedRead && this.ProtectedWrite)
                {
                    return PublicAccessRight.ProtectedReadWrite;
                }

                if (this.ProtectedRead && !this.PublicWritable)
                {
                    return PublicAccessRight.ProtectedReadOnly;
                }

                if (this.PublicReadable && this.ProtectedWrite)
                {
                    return PublicAccessRight.PublicReadProtectedWrite;
                }

                if (this.PublicReadable && !this.PublicWritable)
                {
                    return PublicAccessRight.PublicReadOnly;
                }

                if (this.PublicReadable && this.PublicWritable)
                {
                    return PublicAccessRight.PublicReadWrite;
                }

                return PublicAccessRight.Private;
            }
        }

        /// <summary>
        /// Returns the readability of the authenticated user of this API session.
        /// </summary>
        /// <value>True if the authenticated user of this API session is allowed to read
        /// the spreadsheet represented by this object</value>
        public bool UserReadable
        {
            get { return this.GetBooleanValue("user-access.readable"); }
        }

        /// <summary>
        /// Returns the authenticated user write access right of the spreadsheet.
        /// </summary>
        /// <value>True if the authenticated user of this API session is allowed to write
        /// the spreadsheet represented by this object</value>
        public bool UserWritable
        {
            get { return this.GetBooleanValue("user-access.writable"); }
        }

        /// <summary>
        /// Returns the copyright license of the spreadsheet represented by this object.
        /// </summary>
        public string License
        {
            get { return this.GetValue("license"); }
        }
        
        /// <summary>
        /// Returns the note of the spreadsheet represented by this object.
        /// </summary>
        public string Note
        {
            get { return this.GetValue("note"); }
        }

        /// <summary>
        /// Returns the timezone of the spreadsheet represented by this object.
        /// </summary>
        public string Timezone
        {
            get { return this.GetValue("timezone"); }
        }

        /// <summary>
        /// Returns the last modified time of the spreadsheet represented by this object.
        /// </summary>
        public DateTime LastModifiedTime
        {
            get { return this.GetDateTimeValue("last-modified.time"); }
        }
        #endregion

        #region Spreadsheet Public Access
        /// <summary>
        /// Update the public access right of the spreadsheet represented by this object.
        /// </summary>
        /// <param name="rights">Public access right in combined form</param>
        public void SetPublicAccessRight(PublicAccessRight right)
        {
            SetPublicAccessRightInAtomEntry(this.atomEntry, right);
            this.atomEntry = this.service.Update("spreadsheets/" + this.ResourcePath, this.atomEntry);
        }
        #endregion

        #region Sheet
        /// <summary>
        /// Retrieve the collection of sheets in the spreadsheet represented by this object.
        /// </summary>
        /// <returns>A list of <c>Sheet</c> in the spreadsheet represented by this object</returns>
        public List<Sheet> GetSheets()
        {
            AtomFeed feed = this.service.Query("sheets/" + this.ResourcePath);
            List<Sheet> sheetList = new List<Sheet>();
            foreach (AtomEntry entry in feed.Entries)
            {
                sheetList.Add(new Sheet(entry));
            }
            return sheetList;
        }

        /// <summary>
        /// Create a new sheet in the spreadsheet represented by this object.
        /// </summary>
        /// <param name="sheetName">The name of the sheet to be created</param>
        /// <param name="sheetPosition">The zero-based position index of the sheet to be created</param>
        public void CreateSheet(string sheetName, int sheetPosition)
        {
            AtomEntry entry = new AtomEntry();
            entry.SetValue("sheet-name", sheetName);
            entry.SetValue("sheet-number", sheetPosition.ToString());
            this.service.Create("sheets/" + this.ResourcePath, entry);
        }

        /// <summary>
        /// Delete a sheet in the spreadsheet represented by this object.
        /// </summary>
        /// <param name="sheetName">The name of the sheet to be deleted</param>
        public void DeleteSheet(string sheetName)
        {
            this.service.Delete("sheets/" + this.ResourcePath + "/" + HttpUtility.UrlPathEncode(sheetName));
        }

        /// <summary>
        /// Rename a sheet in the spreadsheet represented by this object.
        /// </summary>
        /// <param name="oldName">The original name of the sheet</param>
        /// <param name="newName">The new name of the sheet</param>
        public void RenameSheet(string oldName, string newName)
        {
            AtomEntry entry = this.service.QueryEntry("sheets/" + this.ResourcePath + "/" + HttpUtility.UrlPathEncode(oldName));
            entry.SetValue("sheet-name", newName);
            this.service.Update("sheets/" + this.ResourcePath + "/" + HttpUtility.UrlPathEncode(oldName), entry);
        }
        #endregion

        #region CellValue
        /// <summary>
        /// Update a cell in a sheet of the spreadsheet represented by this object.
        /// </summary>
        /// <param name="sheetName">The name of the sheet to be updated</param>
        /// <param name="cellPosition">The position of the cell to be updated</param>
        /// <param name="inputValue">The new value of the cell</param>
        public void SetCellValue(string sheetName, CellPosition cellPosition, string inputValue)
        {
            AtomEntry entry = new AtomEntry();
            entry.SetValue("input", inputValue);
            this.service.Update("cells/" + this.ResourcePath + "/" + sheetName + "/" + cellPosition, entry);
        }

        /// <summary>
        /// Get the value of a cell in a sheet of the spreadsheet represented by this object.
        /// </summary>
        /// <param name="sheetName">The name of the sheet to be retrieved</param>
        /// <param name="cellPosition">The position of the cell to be retrieved</param>
        /// <returns>A <c>Cell</c> object contains the value of the cell retrieved.</returns>
        public Cell GetCellValue(string sheetName, CellPosition cellPosition)
        {
            AtomEntry entry = this.service.QueryEntry("cells/" + this.ResourcePath + "/" +
                HttpUtility.UrlPathEncode(sheetName) + "/" + cellPosition.ToString());
            return new Cell(entry);
        }

        /// <summary>
        /// Get the value of a range of cells in a sheet of the spreadsheet represented by this object.
        /// </summary>
        /// <param name="sheetName">The name of the sheet to be retrieved</param>
        /// <param name="range">The range of cells to be retrieved</param>
        /// <returns>A list of <c>Cell</c> objects contain the value of cells retrieved</returns>
        public List<Cell> GetCellValueInRange(string sheetName, CellRange range)
        {
            QueryOption option = new QueryOption();
            option.Range = range;
            AtomFeed feed = this.service.Query("cells/" + this.ResourcePath + "/" + HttpUtility.UrlPathEncode(sheetName),
                option);
            List<Cell> cellList = new List<Cell>();
            foreach (AtomEntry entry in feed.Entries)
            {
                cellList.Add(new Cell(entry));
            }
            return cellList;
        }
        #endregion

        #region SharedWorkspace
        /// <summary>
        /// Returns a list of shared workspaces of the spreadsheet represented by this object.
        /// </summary>
        /// <returns>A list of <c>ShareInfo</c> objects</returns>
        public List<ShareInfo> GetSharedWorkspaces()
        {
            AtomFeed feed = this.service.Query("shares/" + this.ResourcePath);
            List<ShareInfo> shareInfoList = new List<ShareInfo>();
            foreach (AtomEntry entry in feed.Entries)
            {
                shareInfoList.Add(new ShareInfo(entry));
            }
            return shareInfoList;
        }

        /// <summary>
        /// Returns the <c>ShareInfo</c> of a shared workspace of the spreadsheet represented by this object.
        /// </summary>
        /// <param name="workspace">The shared workspace to be query</param>
        /// <returns>A <c>ShareInfo</c> object.</returns>
        public ShareInfo GetShareInfo(Workspace workspace)
        {
            AtomEntry shareInfoEntry = this.service.QueryEntry("shares/" + this.ResourcePath + "/" + workspace.ResourcePath);
            return new ShareInfo(shareInfoEntry);
        }

        /// <summary>
        /// Add a shared workspace to the spreadsheet represented by this object.
        /// </summary>
        /// <param name="workspace">The workspace to be added</param>
        /// <param name="shareWritable">Writability of the workspace to be added</param>
        public void AddSharedWorkspace(Workspace workspace, bool shareWritable)
        {
            AtomEntry entry = new AtomEntry();
            entry.SetValue("organisation", workspace.Organisation);
            entry.SetValue("workspace", workspace.WorkspaceName);
            entry.SetValue("readable", "1");
            entry.SetValue("writable", shareWritable ? "1" : "0");

            this.service.Create("shares/" + this.ResourcePath, entry);
        }

        /// <summary>
        /// Change the access right of a shared workspace to read-only.
        /// </summary>
        /// <param name="workspace">The shared workspace to become read-only</param>
        public void SetSharedWorkspaceReadOnly(Workspace workspace)
        {
            AtomEntry entry = new AtomEntry();
            entry.SetValue("organisation", workspace.Organisation);
            entry.SetValue("workspace", workspace.WorkspaceName);
            entry.SetValue("readable", "1");
            entry.SetValue("writable", "0");

            this.service.Update("shares/" + this.ResourcePath + "/" + workspace.ResourcePath, entry);
        }

        /// <summary>
        /// Change the access right of a shared workspace to read-write.
        /// </summary>
        /// <param name="workspace">The shared workspace to become read-write</param>
        public void SetSharedWorkspaceReadWrite(Workspace workspace)
        {
            AtomEntry entry = new AtomEntry();
            entry.SetValue("organisation", workspace.Organisation);
            entry.SetValue("workspace", workspace.WorkspaceName);
            entry.SetValue("readable", "1");
            entry.SetValue("writable", "1");

            this.service.Update("shares/" + this.ResourcePath + "/" + workspace.ResourcePath, entry);
        }

        /// <summary>
        /// Remove a shared workspace from the spreadsheet represented by this object.
        /// </summary>
        /// <param name="workspace">The shared workspace to be removed</param>
        public void DeleteSharedWorkspace(Workspace workspace)
        {
            this.service.Delete("shares/" + this.ResourcePath + "/" + workspace.ResourcePath);
        }

        #endregion

        #region Action API
        /// <summary>
        /// Change the width of a column in a sheet.
        /// </summary>
        /// <param name="sheetName">The name of the sheet to be changed</param>
        /// <param name="col">The 1-based column index of the column to be resized</param>
        /// <param name="size">The new size</param>
        public void SetColumnWidth(string sheetName, int col, int size)
        {
            Dictionary<string, string> parameters = new Dictionary<string, string>();
            parameters["sheet"] = this.ResourcePath + "/" + sheetName;
            parameters["col"] = col.ToString();
            parameters["size"] = size.ToString();
            this.service.CallAction("spreadsheets/set-column-width", parameters).Close();
        }
        
        /// <summary>
        /// Change the height of a row in a sheet.
        /// </summary>
        /// <param name="sheetName">The name of the sheet to be changed</param>
        /// <param name="col">The 1-based column index of the column to be resized</param>
        /// <param name="size">The new size</param>
        public void SetRowHeight(string sheetName, int col, int size)
        {
            Dictionary<string, string> parameters = new Dictionary<string, string>();
            parameters["sheet"] = this.ResourcePath + "/" + sheetName;
            parameters["row"] = col.ToString();
            parameters["size"] = size.ToString();
            this.service.CallAction("spreadsheets/set-row-height", parameters).Close();
        }

        /// <summary>
        /// Export the spreadsheet represented by this object as a format specified by <paramref name="contentType"/>.
        /// </summary>
        /// <param name="contentType">The exporting format in MIME type</param>
        /// <returns>A stream for reading the exported file</returns>
        public Stream Export(string contentType)
        {
            Dictionary<string, string> parameters = new Dictionary<string, string>();
            parameters["spreadsheet"] = HttpUtility.UrlDecode(this.ResourcePath);
            parameters["content-type"] = contentType;
            EditGridHttpResponse res = this.service.CallAction("spreadsheets/export", parameters);
            return res.ResponseStream;
        }

        /// <summary>
        /// Update a region of cells in a sheet of the spreadsheet represented by this object.
        /// </summary>
        /// <param name="sheetName">The name of the sheet to be updated</param>
        /// <param name="cellRef">The region to be updated</param>
        /// <param name="region">The new value of the region</param>
        public void UpdateRegion(string sheetName, CellRef cellRef, String[][] inputValues)
        {
            string csv = CSVUtil.AsCSV(inputValues);

            UriBuilder ub = new UriBuilder(new Uri(this.service.BaseUri, "rest/1/action/spreadsheets/update-region-csv"));
            Dictionary<string, string> parameters = new Dictionary<string, string>();
            parameters["region"] = this.ResourcePath + "/" + sheetName + "/" + cellRef;
            QueryOption option = new QueryOption(parameters);
            ub.Query = option.ToString();

            EditGridHttpRequest req = new EditGridHttpRequest("POST", ub.Uri, this.service.AppId);
            req.ApiToken = this.service.ApiToken;
            req.ContentType = "text/x-comma-separated-values";
            req.SetRequestBody(csv);
            EditGridHttpResponse res = req.GetResponse();
            if (res.StatusCode != HttpStatusCode.OK) {
                throw new EditGridServiceException(res.ResponseText);
            }
            res.Close();
        }

        /// <summary>
        /// Mark a revision
        /// </summary>
        /// <param name="revisionNote">Revision note</param>
        public void MarkRevision(string revisionNote)
        {

            UriBuilder ub = new UriBuilder(new Uri(this.service.BaseUri, "rest/1/action/spreadsheets/mark-revision"));
            Dictionary<string, string> parameters = new Dictionary<string, string>();
            parameters["spreadsheet"] = this.ResourcePath;
            parameters["note"] = revisionNote;
            QueryOption option = new QueryOption(parameters);
            ub.Query = option.ToString();

            EditGridHttpRequest req = new EditGridHttpRequest("POST", ub.Uri, this.service.AppId);
            req.ApiToken = this.service.ApiToken;
            req.SetRequestBody("");
            EditGridHttpResponse res = req.GetResponse();
            if (res.StatusCode != HttpStatusCode.OK)
            {
                throw new EditGridServiceException(res.ResponseText);
            }
            res.Close();
        }

        public void ReplaceSheetWithFile(string sheetName, Stream stream, string contentType, int srcSheetNum)
        {
            UriBuilder ub = new UriBuilder(new Uri(this.service.BaseUri, "rest/1/action/spreadsheets/replace-sheet-with-file"));
            Dictionary<string, string> parameters = new Dictionary<string, string>();
            parameters["sheet"] = this.ResourcePath + "/" + sheetName;
            parameters["content-type"] = contentType;
            parameters["sheet-num"] = srcSheetNum.ToString();
            QueryOption option = new QueryOption(parameters);
            ub.Query = option.ToString();

            EditGridHttpRequest req = new EditGridHttpRequest("POST", ub.Uri, this.service.AppId);
            req.ApiToken = this.service.ApiToken;
            req.SetRequestBody(stream);
            EditGridHttpResponse res = req.GetResponse();
            if (res.StatusCode != HttpStatusCode.OK)
            {
                throw new EditGridServiceException(res.ResponseText);
            }
            res.Close();
        }

        public void ReplaceSpreadsheetWithFile(Stream stream, string contentType)
        {
            UriBuilder ub = new UriBuilder(new Uri(this.service.BaseUri, "rest/1/action/spreadsheets/replace-spreadsheet-with-file"));
            Dictionary<string, string> parameters = new Dictionary<string, string>();
            parameters["spreadsheet"] = this.ResourcePath;
            parameters["content-type"] = contentType;
            QueryOption option = new QueryOption(parameters);
            ub.Query = option.ToString();

            EditGridHttpRequest req = new EditGridHttpRequest("POST", ub.Uri, this.service.AppId);
            req.ApiToken = this.service.ApiToken;
            req.SetRequestBody(stream);
            EditGridHttpResponse res = req.GetResponse();
            if (res.StatusCode != HttpStatusCode.OK)
            {
                throw new EditGridServiceException(res.ResponseText);
            }
            res.Close();
        }
        #endregion

        #region Internal helper functions
        internal static void SetPublicAccessRightInAtomEntry(AtomEntry entry, PublicAccessRight rights)
        {
            switch (rights)
            {
                case PublicAccessRight.Private:
                    entry.SetValue("public-access.readable", "0");
                    entry.SetValue("public-access.writable", "0");
                    entry.SetValue("public-access.protected-read", "0");
                    entry.SetValue("public-access.protected-write", "0");
                    break;
                case PublicAccessRight.PublicReadOnly:
                    entry.SetValue("public-access.readable", "1");
                    entry.SetValue("public-access.writable", "0");
                    entry.SetValue("public-access.protected-read", "0");
                    entry.SetValue("public-access.protected-write", "0");
                    break;
                case PublicAccessRight.PublicReadWrite:
                    entry.SetValue("public-access.readable", "1");
                    entry.SetValue("public-access.writable", "1");
                    entry.SetValue("public-access.protected-read", "0");
                    entry.SetValue("public-access.protected-write", "0");
                    break;
                case PublicAccessRight.PublicReadProtectedWrite:
                    entry.SetValue("public-access.readable", "1");
                    entry.SetValue("public-access.writable", "1");
                    entry.SetValue("public-access.protected-read", "0");
                    entry.SetValue("public-access.protected-write", "1");
                    break;
                case PublicAccessRight.ProtectedReadOnly:
                    entry.SetValue("public-access.readable", "1");
                    entry.SetValue("public-access.writable", "0");
                    entry.SetValue("public-access.protected-read", "1");
                    entry.SetValue("public-access.protected-write", "0");
                    break;
                case PublicAccessRight.ProtectedReadWrite:
                    entry.SetValue("public-access.readable", "1");
                    entry.SetValue("public-access.writable", "1");
                    entry.SetValue("public-access.protected-read", "1");
                    entry.SetValue("public-access.protected-write", "1");
                    break;
            }
        }
        #endregion
    }
}
